%%% Copyright 2010 Google Inc.
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%%     http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.

%%% @copyright 2010 Google Inc. All Rights Reserved.
%%% @author <jacoby@google.com> Jacoby Thwaites [http://sites/madcloud]
%%% @version {@version}
%%% @doc
%%% Web utility functions, used by web_srv and web_view modules.
%%%
%%% @headerfile "../include/txm.hrl"

-module(web_util).

-include("txm.hrl").
-include("web_srv.hrl").

-export([query2proplist/1, url_decode/1,client_redirect/2]).
-export([send_response/3, list_to_term/1]).
-export([parts/2, form_data/2]).

-define(OPTIONS, [{return, list}]).

%% @doc Retrieves the content from the multipart/form-data whose name
%% attribute matches the supplied name. Note that the first argument
%% must be the result of a call to parts/2.
%% @spec form_data(list(), string()) -> string()
form_data([], _Name) -> ?REASON_NOT_FOUND;
form_data([{Headers, Content} | Parts], Name) ->
  {?FORM_DATA_PART, Attrs} =
      proplists:get_value(?CONTENT_DISPOSITION_HEADER, Headers),
  case proplists:get_value(?FORM_DATA_NAME, Attrs) of
      Name -> Content;
      _Other -> form_data(Parts, Name)
  end.

%% @doc Parses the supplied unparsed string content into multipart/form-data
%% parts using the supplied boundary string.
parts(String, Boundary) ->
  PrefixedBoundary = ?FORM_DATA_BOUNDARY_PREFIX ++ Boundary,

  % The regex isolates parts, using an assertion (?=...) to identify
  % terminating boundaries without consuming them.
  % Options: ungreedy = shortest possible length of capture group.
  %          global = iterate after first match is found
  %          dotall = include \r\n in the (.+) capture group.
  Re = PrefixedBoundary ++ "\r\n(.+)(?=\r\n" ++ PrefixedBoundary ++ ")",
  Options = [{capture, all_but_first, list}, ungreedy, global, dotall],

  % Each element of MatchLists is a list of captured strings, each of which
  % is only one element long since there's only 1 capture group in the regex.
  {match, MatchLists} = re:run(String, Re, Options),
  Parts = [Part || [Part] <- MatchLists],
  controls(Parts).

controls(Parts) ->
    [control(Part) || Part <- Parts].

control(Part) ->
  [Headers, Content] = re:split(Part, ?HTTP_END_HEADER, ?OPTIONS),
  {[header(Header) ||
      Header <- re:split(Headers, ?HTTP_LINE_BREAK, ?OPTIONS)],
   Content}.

header(Header) ->
    [Hv | Attrs] = re:split(Header, "; ?", ?OPTIONS),
    [HeaderName, HeaderValue] = re:split(Hv, ": ?", ?OPTIONS),
    {HeaderName, {HeaderValue, [attr(Attr) || Attr <- Attrs]}}.

attr(Attr) ->
    {match, [Name, Value]} =
        re:run(Attr, "(.+)=\"(.+)\"", [{capture, all_but_first, list}]),
    {Name, Value}.

%% @doc Returns a proplist containing name/value pairs from a querystring.
%% The name and value are both URL decoded.
%% Duplicate keys may be returned. Use proplist: library to manipulate.
%% @spec query2proplist(string()) -> list()
query2proplist(QueryString) ->
  [url_decode(list_to_tuple(string:tokens(Pair, "="))) ||
     Pair <- string:tokens(QueryString, "&")].

%% @doc Returns the url decoded version of the tuple or string argument.
%% The decoding is not complete to specification, but serves our needs.
%% @spec url_decode(tuple() | string()) -> tuple() | string()
url_decode({Name, Value}) ->
  {url_decode(Name), url_decode(Value)};
url_decode({Name}) -> {url_decode(Name), ""};

%% #2
url_decode([]) -> [];
url_decode([$+ | Rest]) -> [$ | url_decode(Rest)];
url_decode([$%, Hi, Lo | Rest]) ->
  [httpd_util:hexlist_to_integer([Hi, Lo]) | url_decode(Rest)];
url_decode([Other | Rest]) -> [Other | url_decode(Rest)].

%% @doc Sends an HTTP redirect to the client, to the supplied `Href'.
%% @spec client_redirect(term(), string()) -> ok
client_redirect(SessionId, Href) ->
  Response = [?HTTP_HTML_HEADER,
              ?HTTP_END_HEADER,
              "<html><head>"
              "<meta http-equiv=\"Refresh\" content=\"0; url=",
              Href,
              "\" />"
              "</head><body>"
              "Please redirect to <a href=\"",
              Href,
              "\">here</a>"
              "</body></html>"],
  mod_esi:deliver(SessionId, Response).

%% @doc Sends a single response back to the supplied `SessionId', using
%% the `json' querystring param to determine JSON or XML format.
send_response(Args, SessionId, Struct) ->
  case proplists:get_value(?QUERY_JSON, Args, "false") of
    "" -> send_json_response(Args, SessionId, Struct);
    "true" -> send_json_response(Args, SessionId, Struct);
    "false" -> send_xml_response(Args, SessionId, Struct)
  end.

%% @doc Sends a single JSON or JSONP response to the supplied `SessionId'.
%% @spec send_json_response(proplist(), term(), term()) -> ok
send_json_response(Args, SessionId, Struct) ->
  case proplists:get_value(?QUERY_CALLBACK, Args, ?UNDEFINED) of
    ?UNDEFINED ->
      Response = [?HTTP_JSON_HEADER,
              ?HTTP_END_HEADER
              | xj:struct_to_json(Struct)];
    Callback ->
      Response = [?HTTP_JSON_HEADER,
                  ?HTTP_END_HEADER,
                  Callback, "(", xj:struct_to_json(Struct), ")"]
  end,
  mod_esi:deliver(SessionId, Response).

%% @doc Sends a single XML response with XML headers to the supplied
%% `SessionId'.
%% @spec send_xml_response(proplist(), term(), term()) -> ok
send_xml_response(Args, SessionId, Struct) ->
  Response =
    case {xsl, proplists:get_value(?QUERY_XSL, Args, ?UNDEFINED)} of
      {xsl, ?UNDEFINED} ->
        [?HTTP_XML_HEADER,
         ?HTTP_END_HEADER,
         ?XML_DECLARATION
         | xj:struct_to_xml(Struct)];
      {xsl, Href} ->
        [?HTTP_XML_HEADER,
         ?HTTP_END_HEADER,
         ?XML_DECLARATION,
         ?XSL_DECLARATION(Href)
         | xj:struct_to_xml(Struct)]
    end,
  mod_esi:deliver(SessionId, Response).

%% @doc Parse a string into a term. A period is appended to
%% the term before scanning - make sure there isn't one on
%% the supplied `String'.
%% @spec list_to_term(string()) -> term()
list_to_term(String) ->
  ?OK_RETURN(Tokens,_) = erl_scan:string(String ++ "."),
  ?OK_RETURN(Term) = erl_parse:parse_term(Tokens),
  Term.
